package com.example.docmanagement.service;

import com.example.docmanagement.config.MinioConfig;
import com.example.docmanagement.dto.request.FileMetadataQueryRequest;
import com.example.docmanagement.dto.request.FileUploadRequest;
import com.example.docmanagement.dto.response.FileMetadataResponse;
import com.example.docmanagement.dto.response.PageResponse;
import com.example.docmanagement.entity.FileMetadata;
import com.example.docmanagement.exception.ResourceNotFoundException;
import com.example.docmanagement.repository.FileMetadataRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import jakarta.persistence.criteria.Predicate;
import net.coobird.thumbnailator.Thumbnails;

/**
 * 文件元数据服务
 * 负责文件元数据的业务逻辑处理
 * 
 * 核心功能：
 * - 文件上传（图片、文档、Markdown）
 * - 文件信息管理
 * - 文件查询和统计
 * - 文件删除（软删除/物理删除）
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class FileMetadataService {

    private final FileMetadataRepository fileMetadataRepository;
    private final MinioService minioService;
    private final MinioConfig minioConfig;

    // ============================================================================
    // 文件上传
    // ============================================================================

    /**
     * 上传文件
     * 
     * @param file       上传的文件
     * @param request    上传请求参数
     * @param uploadedBy 上传者 ID
     * @return 文件元数据
     */
    @Transactional
    public FileMetadata uploadFile(MultipartFile file, FileUploadRequest request, UUID uploadedBy) {
        try {
            // 1. 验证文件
            validateFile(file, request);

            // 2. 确定文件类型和 Bucket
            String fileType = determineFileType(file, request.getFileType());
            // 对于 Monaco 编辑器上传的 PDF，使用公开的 doc-images 桶，以便在 Markdown 中直接访问
            String bucketName = determineBucketName(fileType, request.getUploadSource());

            // 3. 上传到 MinIO
            String filePath = minioService.uploadFile(file, bucketName);
            String objectKey = filePath.substring(filePath.indexOf('/') + 1);

            // 4. 计算 MD5 哈希
            String md5Hash = calculateMD5(file.getBytes());

            // 5. 构建文件元数据
            FileMetadata.FileMetadataBuilder builder = FileMetadata.builder()
                    .fileName(file.getOriginalFilename())
                    .filePath(filePath)
                    .bucketName(bucketName)
                    .objectKey(objectKey)
                    .fileSize(file.getSize())
                    .mimeType(file.getContentType())
                    .fileExtension(getFileExtension(file.getOriginalFilename()))
                    .fileType(fileType)
                    .fileCategory(request.getFileCategory())
                    .md5Hash(md5Hash)
                    .accessPolicy(request.getAccessPolicy() != null ? request.getAccessPolicy()
                            : (isPrivateFileType(fileType, request.getUploadSource()) ? "private" : "public"))
                    .isPublic(!isPrivateFileType(fileType, request.getUploadSource()))
                    .documentId(request.getDocumentId())
                    .uploadedBy(uploadedBy)
                    .uploadSource(request.getUploadSource() != null ? request.getUploadSource() : "attachment");

            // 6. 如果是图片，提取尺寸信息
            if ("image".equals(fileType)) {
                extractImageDimensions(file, builder);

                // 7. 生成缩略图（如果需要）
                if (Boolean.TRUE.equals(request.getGenerateThumbnail())) {
                    generateAndUploadThumbnail(file, filePath, builder);
                }
            }

            // 8. 保存到数据库
            FileMetadata fileMetadata = builder.build();
            return fileMetadataRepository.save(fileMetadata);

        } catch (Exception e) {
            log.error("文件上传失败", e);
            throw new RuntimeException("文件上传失败: " + e.getMessage(), e);
        }
    }

    /**
     * 上传 Markdown 文本到 MinIO
     * 
     * @param content    Markdown 文本内容
     * @param fileName   文件名（可选）
     * @param uploadedBy 上传者 ID
     * @return 文件元数据
     */
    @Transactional
    public FileMetadata uploadMarkdownText(String content, String fileName, UUID uploadedBy) {
        try {
            // 1. 上传到 MinIO
            String bucketName = minioConfig.getBuckets().getDocContents();
            String filePath = minioService.uploadText(content, bucketName, fileName);
            String objectKey = filePath.substring(filePath.indexOf('/') + 1);

            // 2. 计算 MD5
            String md5Hash = calculateMD5(content.getBytes(StandardCharsets.UTF_8));

            // 3. 构建元数据
            FileMetadata fileMetadata = FileMetadata.builder()
                    .fileName(fileName != null ? fileName : "document.md")
                    .filePath(filePath)
                    .bucketName(bucketName)
                    .objectKey(objectKey)
                    .fileSize((long) content.getBytes(StandardCharsets.UTF_8).length)
                    .mimeType("text/markdown")
                    .fileExtension(".md")
                    .fileType("markdown")
                    .fileCategory("doc-content")
                    .md5Hash(md5Hash)
                    .accessPolicy("public")
                    .isPublic(true)
                    .uploadedBy(uploadedBy)
                    .uploadSource("manual") // Markdown 文本上传标记为 manual
                    .build();

            return fileMetadataRepository.save(fileMetadata);

        } catch (Exception e) {
            log.error("Markdown 文本上传失败", e);
            throw new RuntimeException("Markdown 上传失败: " + e.getMessage(), e);
        }
    }

    // ============================================================================
    // 文件查询
    // ============================================================================

    /**
     * 根据 ID 查询文件
     * 
     * @param id 文件 ID
     * @return 文件元数据
     */
    public FileMetadata findById(Long id) {
        return fileMetadataRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("文件不存在"));
    }

    /**
     * 查询文档关联的所有文件
     * 
     * @param documentId 文档 ID
     * @return 文件列表
     */
    public List<FileMetadata> findByDocumentId(Long documentId) {
        return fileMetadataRepository.findByDocumentIdAndDeletedAtIsNull(documentId);
    }

    /**
     * 查询文档特定类型的文件
     * 
     * @param documentId 文档 ID
     * @param fileType   文件类型
     * @return 文件列表
     */
    public List<FileMetadata> findByDocumentIdAndType(Long documentId, String fileType) {
        return fileMetadataRepository.findByDocumentIdAndFileTypeAndDeletedAtIsNull(documentId, fileType);
    }

    /**
     * 分页查询文件列表（默认排除软删除）
     */
    @Transactional(readOnly = true)
    public PageResponse<FileMetadataResponse> getFiles(FileMetadataQueryRequest request) {
        // 构建排序
        Sort.Direction direction = "asc".equalsIgnoreCase(request.getSortOrder())
                ? Sort.Direction.ASC
                : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, mapSortProperty(request.getSortBy()));
        Pageable pageable = PageRequest.of(Math.max(request.getPage() - 1, 0), request.getPageSize(), sort);

        Specification<FileMetadata> specification = buildSpecification(request);
        Page<FileMetadata> page = fileMetadataRepository.findAll(specification, pageable);

        List<FileMetadataResponse> data = page.getContent()
                .stream()
                .map(this::toResponse)
                .collect(Collectors.toList());

        return PageResponse.<FileMetadataResponse>builder()
                .data(data)
                .page(request.getPage())
                .pageSize(request.getPageSize())
                .total(page.getTotalElements())
                .totalPages(page.getTotalPages())
                .hasNext(page.hasNext())
                .hasPrevious(page.hasPrevious())
                .build();
    }

    // ============================================================================
    // 文件删除
    // ============================================================================

    /**
     * 删除文件（软删除）
     * 
     * @param id        文件 ID
     * @param deletedBy 删除者 ID
     */
    @Transactional
    public void softDelete(Long id, UUID deletedBy) {
        FileMetadata file = findById(id);
        file.setDeletedAt(LocalDateTime.now());
        file.setDeletedBy(deletedBy);
        fileMetadataRepository.save(file);
        log.info("文件软删除成功: {}", id);
    }

    /**
     * 删除文件（物理删除：数据库 + MinIO）
     * 
     * @param id 文件 ID
     */
    @Transactional
    public void permanentDelete(Long id) {
        FileMetadata file = findById(id);

        try {
            // 1. 从 MinIO 删除文件
            minioService.deleteFile(file.getFilePath());

            // 2. 如果有缩略图，也删除
            if (Boolean.TRUE.equals(file.getHasThumbnail()) && file.getThumbnailPath() != null) {
                minioService.deleteFile(file.getThumbnailPath());
            }

            // 3. 从数据库删除记录
            fileMetadataRepository.delete(file);

            log.info("文件物理删除成功: {}", id);
        } catch (Exception e) {
            log.error("文件物理删除失败: " + id, e);
            throw new RuntimeException("文件删除失败: " + e.getMessage(), e);
        }
    }

    // ============================================================================
    // DTO 转换
    // ============================================================================

    /**
     * 实体转 DTO
     * 
     * @param entity 文件元数据实体
     * @return DTO
     */
    public FileMetadataResponse toResponse(FileMetadata entity) {
        return FileMetadataResponse.builder()
                .id(entity.getId())
                .fileName(entity.getFileName())
                .filePath(entity.getFilePath())
                .fileUrl(minioService.getPublicUrl(entity.getFilePath()))
                .thumbnailUrl(
                        entity.getThumbnailPath() != null ? minioService.getPublicUrl(entity.getThumbnailPath()) : null)
                .fileSize(entity.getFileSize())
                .mimeType(entity.getMimeType())
                .fileType(entity.getFileType())
                .fileExtension(entity.getFileExtension())
                .width(entity.getWidth())
                .height(entity.getHeight())
                .bucketName(entity.getBucketName())
                .accessPolicy(entity.getAccessPolicy())
                .md5Hash(entity.getMd5Hash())
                .documentId(entity.getDocumentId())
                .uploadedBy(entity.getUploadedBy())
                .downloadCount(entity.getDownloadCount())
                .viewCount(entity.getViewCount())
                .createdAt(entity.getCreatedAt())
                .updatedAt(entity.getUpdatedAt())
                .uploadSource(entity.getUploadSource())
                .build();
    }

    // ============================================================================
    // 私有工具方法
    // ============================================================================

    /**
     * 验证文件
     */
    private void validateFile(MultipartFile file, FileUploadRequest request) {
        // 检查文件是否为空
        if (file.isEmpty()) {
            throw new IllegalArgumentException("文件不能为空");
        }

        // 检查文件大小
        Long maxFileSize = minioConfig.getUpload().getMaxFileSize();
        if (file.getSize() > maxFileSize) {
            throw new IllegalArgumentException(
                    String.format("文件过大，最大支持 %dMB", maxFileSize / 1024 / 1024));
        }

        // 检查文件类型
        String contentType = file.getContentType();
        if (contentType == null) {
            throw new IllegalArgumentException("无法确定文件类型");
        }

        // TODO: 使用 Apache Tika 进行 MIME 类型检测，防止伪造
    }

    /**
     * 确定文件类型
     */
    private String determineFileType(MultipartFile file, String requestedType) {
        if (requestedType != null && !requestedType.isEmpty()) {
            return requestedType;
        }

        String contentType = file.getContentType();
        if (contentType == null) {
            return "other";
        }

        if (contentType.startsWith("image/")) {
            return "image";
        } else if (contentType.equals("text/markdown") ||
                file.getOriginalFilename().endsWith(".md")) {
            return "markdown";
        } else if (contentType.equals("application/pdf") ||
                contentType.contains("word") ||
                contentType.contains("excel") ||
                contentType.contains("powerpoint")) {
            return "document";
        } else {
            return "other";
        }
    }

    /**
     * 确定 Bucket 名称
     * 
     * @param fileType     文件类型
     * @param uploadSource 上传来源（editor/attachment/manual）
     */
    private String determineBucketName(String fileType, String uploadSource) {
        MinioConfig.BucketConfig buckets = minioConfig.getBuckets();

        // 对于 Monaco 编辑器上传的文档（PDF 等），使用公开的 doc-images 桶，以便在 Markdown 中直接访问
        if ("document".equals(fileType) && "editor".equals(uploadSource)) {
            return buckets.getDocImages();
        }

        // Java 11 兼容的 switch 语句
        switch (fileType) {
            case "image":
                return buckets.getDocImages();
            case "markdown":
                return buckets.getDocContents();
            case "document":
                return buckets.getDocAttachments();
            case "avatar":
                return buckets.getUserAvatars();
            case "cover":
                return buckets.getDocImages();
            default:
                return buckets.getTempUploads();
        }
    }

    /**
     * 判断是否为私有文件类型
     * 
     * @param fileType     文件类型
     * @param uploadSource 上传来源（editor/attachment/manual）
     */
    private boolean isPrivateFileType(String fileType, String uploadSource) {
        // Monaco 编辑器上传的文档（PDF）使用公开桶，所以不是私有的
        if ("document".equals(fileType) && "editor".equals(uploadSource)) {
            return false;
        }
        return "document".equals(fileType) || "markdown".equals(fileType);
    }

    /**
     * 获取文件扩展名
     */
    private String getFileExtension(String filename) {
        if (filename == null || filename.isEmpty()) {
            return "";
        }
        int dotIndex = filename.lastIndexOf('.');
        if (dotIndex == -1) {
            return "";
        }
        return filename.substring(dotIndex);
    }

    /**
     * 计算 MD5 哈希
     */
    private String calculateMD5(byte[] bytes) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(bytes);
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            log.error("MD5 计算失败", e);
            return null;
        }
    }

    /**
     * 提取图片尺寸信息
     */
    private void extractImageDimensions(MultipartFile file, FileMetadata.FileMetadataBuilder builder) {
        try {
            BufferedImage image = ImageIO.read(file.getInputStream());
            if (image != null) {
                builder.width(image.getWidth());
                builder.height(image.getHeight());
            }
        } catch (IOException e) {
            log.warn("无法读取图片尺寸: {}", file.getOriginalFilename());
        }
    }

    /**
     * 生成并上传缩略图
     * 使用 Thumbnailator 库生成缩略图，并上传到 thumbnails bucket
     * 
     * @param file         原始图片文件
     * @param originalPath 原始文件路径（用于生成缩略图文件名）
     * @param builder      文件元数据构建器
     */
    private void generateAndUploadThumbnail(MultipartFile file, String originalPath,
            FileMetadata.FileMetadataBuilder builder) {
        try {
            // 1. 读取原始图片
            BufferedImage originalImage = ImageIO.read(file.getInputStream());
            if (originalImage == null) {
                log.warn("无法读取图片，跳过缩略图生成: {}", file.getOriginalFilename());
                builder.hasThumbnail(false);
                return;
            }

            // 2. 获取缩略图配置（默认 200x200）
            int thumbnailWidth = minioConfig.getUpload().getThumbnailWidth();
            int thumbnailHeight = minioConfig.getUpload().getThumbnailHeight();
            float quality = minioConfig.getUpload().getImageQuality();

            // 3. 生成缩略图（保持宽高比，等比例缩放）
            ByteArrayOutputStream thumbnailOutputStream = new ByteArrayOutputStream();
            Thumbnails.of(originalImage)
                    .size(thumbnailWidth, thumbnailHeight) // 最大尺寸，保持宽高比
                    .outputFormat(getThumbnailFormat(file.getOriginalFilename())) // 保持原格式或转换为 JPEG
                    .outputQuality(quality) // 压缩质量
                    .toOutputStream(thumbnailOutputStream);

            byte[] thumbnailBytes = thumbnailOutputStream.toByteArray();
            log.debug("缩略图生成成功: {}x{}, 大小: {} bytes", thumbnailWidth, thumbnailHeight, thumbnailBytes.length);

            // 4. 生成缩略图文件名（在原文件名基础上添加 _thumb 后缀）
            String thumbnailFileName = generateThumbnailFileName(file.getOriginalFilename());

            // 5. 上传缩略图到 thumbnails bucket
            String thumbnailBucket = minioConfig.getBuckets().getThumbnails();
            String thumbnailPath = minioService.uploadBytes(
                    thumbnailBytes,
                    thumbnailBucket,
                    thumbnailFileName,
                    getThumbnailContentType(file.getOriginalFilename()));

            // 6. 生成缩略图访问 URL（thumbnails bucket 是公开的）
            String thumbnailUrl = minioService.getPublicUrl(thumbnailPath);

            // 7. 设置缩略图相关字段
            builder.hasThumbnail(true)
                    .thumbnailPath(thumbnailPath)
                    .thumbnailUrl(thumbnailUrl);

            log.info("缩略图生成并上传成功: {}", thumbnailPath);

        } catch (Exception e) {
            log.error("缩略图生成失败: {}", file.getOriginalFilename(), e);
            // 缩略图生成失败不影响主流程，只记录错误
            builder.hasThumbnail(false);
        }
    }

    /**
     * 生成缩略图文件名
     * 格式：原文件名_thumb.扩展名
     * 示例：image.jpg -> image_thumb.jpg
     * 
     * @param originalFilename 原始文件名
     * @return 缩略图文件名
     */
    private String generateThumbnailFileName(String originalFilename) {
        if (originalFilename == null || originalFilename.isEmpty()) {
            return "thumbnail_" + UUID.randomUUID() + ".jpg";
        }

        int dotIndex = originalFilename.lastIndexOf('.');
        if (dotIndex == -1) {
            // 没有扩展名，添加 _thumb 后缀和 .jpg 扩展名
            return originalFilename + "_thumb.jpg";
        }

        String nameWithoutExt = originalFilename.substring(0, dotIndex);
        String extension = originalFilename.substring(dotIndex);

        // 如果原图是 GIF，缩略图转换为 JPEG（GIF 动画不支持）
        if (extension.toLowerCase().equals(".gif")) {
            return nameWithoutExt + "_thumb.jpg";
        }

        return nameWithoutExt + "_thumb" + extension;
    }

    /**
     * 获取缩略图输出格式
     * 对于 GIF 等格式，转换为 JPEG
     * 
     * @param originalFilename 原始文件名
     * @return 输出格式（jpg, png 等）
     */
    private String getThumbnailFormat(String originalFilename) {
        if (originalFilename == null || originalFilename.isEmpty()) {
            return "jpg";
        }

        String lowerName = originalFilename.toLowerCase();
        if (lowerName.endsWith(".gif")) {
            // GIF 动画不支持，转换为 JPEG
            return "jpg";
        } else if (lowerName.endsWith(".png")) {
            return "png";
        } else if (lowerName.endsWith(".webp")) {
            return "webp";
        } else {
            // 默认使用 JPEG（兼容性好，文件小）
            return "jpg";
        }
    }

    /**
     * 获取缩略图 MIME 类型
     * 
     * @param originalFilename 原始文件名
     * @return MIME 类型
     */
    private String getThumbnailContentType(String originalFilename) {
        String format = getThumbnailFormat(originalFilename);
        switch (format) {
            case "png":
                return "image/png";
            case "webp":
                return "image/webp";
            case "jpg":
            default:
                return "image/jpeg";
        }
    }

    /**
     * 构建查询 Specification
     */
    private Specification<FileMetadata> buildSpecification(FileMetadataQueryRequest request) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            // 排除软删除
            predicates.add(cb.isNull(root.get("deletedAt")));

            if (request.getBucketName() != null && !request.getBucketName().isEmpty()) {
                predicates.add(cb.equal(root.get("bucketName"), request.getBucketName()));
            }
            if (request.getFileType() != null && !request.getFileType().isEmpty()) {
                predicates.add(cb.equal(root.get("fileType"), request.getFileType()));
            }
            if (request.getFileCategory() != null && !request.getFileCategory().isEmpty()) {
                predicates.add(cb.equal(root.get("fileCategory"), request.getFileCategory()));
            }
            if (request.getDocumentId() != null) {
                predicates.add(cb.equal(root.get("documentId"), request.getDocumentId()));
            }
            if (request.getUploadedBy() != null) {
                predicates.add(cb.equal(root.get("uploadedBy"), request.getUploadedBy()));
            }
            if (request.getAccessPolicy() != null && !request.getAccessPolicy().isEmpty()) {
                predicates.add(cb.equal(root.get("accessPolicy"), request.getAccessPolicy()));
            }
            if (request.getKeyword() != null && !request.getKeyword().isEmpty()) {
                String pattern = "%" + request.getKeyword().toLowerCase() + "%";
                predicates.add(cb.or(
                        cb.like(cb.lower(root.get("fileName")), pattern),
                        cb.like(cb.lower(root.get("objectKey")), pattern)));
            }

            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    /**
     * 映射排序字段，防止非法字段注入
     */
    private String mapSortProperty(String sortBy) {
        if (sortBy == null || sortBy.isEmpty()) {
            return "createdAt";
        }
        switch (sortBy) {
            case "updatedAt":
                return "updatedAt";
            case "fileSize":
                return "fileSize";
            case "fileName":
                return "fileName";
            default:
                return "createdAt";
        }
    }
}
